﻿using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DZParallelLib.Utility
{
    /// <summary>
    /// Controls locking for a hierarchical image set in a reasonably efficient and async way.
    /// </summary>
    class CacheFileLocker
    {
        /// <summary>
        /// Lock a particular cacheIndex so others can't use it. We sit in await hell until
        /// we can get it!
        /// </summary>
        /// <param name="cacheIndex"></param>
        /// <returns></returns>
        internal async Task<IDisposable> Lock(Tuple<int, ulong, ulong> cacheIndex)
        {
            // Get sole access to the list of locks.

            LockInfo myLock = null;

            using (var lockMe = await _masterTableLock.LockAsync())
            {
                // See if a lock has been created already. If so, we grab it, otherwise we create one
                if (!_lockLookup.TryGetValue(cacheIndex, out myLock))
                {
                    myLock = new LockInfo();
                    _lockLookup[cacheIndex] = myLock;
                }
                myLock._count++;
            }

            //
            // We have a lock now. Next, we need to grab it, and return the code that will be able to properly
            // unlock it.
            //

            var unlockCachLock = await myLock._lock.LockAsync();

            //
            // We return a disposable object that will clean everything up. Note that we have IDisposable here, and we
            // wait in it. It should be very fast, but I know it isn't "happy" as it could deadlock the dispoing thread, which is
            // non-happy. :-)
            //
            // TODO to fix. The right way is to kill off the disposable, and force people to make a try/finally block.
            //

            return new LockerUnlocker(
                () =>
                {
                    unlockCachLock.Dispose();
                    Unlock(cacheIndex).Wait();
                }
                );
        }

        /// <summary>
        /// When we're unlocking a guy, see if we should delete it or not. Only called by our code.
        /// </summary>
        /// <param name="cacheIndex"></param>
        /// <returns></returns>
        private async Task Unlock(Tuple<int, ulong, ulong> cacheIndex)
        {
            using (var lockMe = await _masterTableLock.LockAsync())
            {
                LockInfo myLock;
                if (!_lockLookup.TryGetValue(cacheIndex, out myLock))
                {
                    throw new InvalidOperationException("Attempt to get a lock that no longer exists!");
                }
                myLock._count--;
                if (myLock._count == 0)
                    _lockLookup.Remove(cacheIndex);
            }
        }

        /// <summary>
        /// We have to release the lock, and perhaps clean up the dictionary when we do the
        /// release!
        /// </summary>
        private class LockerUnlocker : IDisposable
        {
            public LockerUnlocker(Action doWhenDone)
            {
                _doWhenDone = doWhenDone;
            }

            private Action _doWhenDone;

            public void Dispose()
            {
                _doWhenDone();
            }
        }

        /// <summary>
        /// Lock controlling the master lock table.
        /// </summary>
        private AsyncLock _masterTableLock = new AsyncLock();

        /// <summary>
        /// Hold the info for a lock so we know when to kill it, among other things.
        /// </summary>
        private class LockInfo
        {
            /// <summary>
            /// Reference count. When it hits zero, we can get rid of it.
            /// </summary>
            public int _count = 0;

            public AsyncLock _lock = new AsyncLock();
        }

        /// <summary>
        /// All the active locks that are sitting around right now.
        /// </summary>
        private Dictionary<Tuple<int, ulong, ulong>, LockInfo> _lockLookup = new Dictionary<Tuple<int, ulong, ulong>, LockInfo>();
    }
}
